package com.dana.judge.service.impl;

import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.date.StopWatch;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import com.core.constants.Constants;
import com.core.constants.JudgeConstants;
import com.core.enums.CodeRunStatus;
import com.dana.judge.callback.DockerStartResultCallback;
import com.dana.judge.callback.StatisticsCallback;
import com.dana.judge.domain.CompileResult;
import com.dana.judge.domain.SandBoxExecuteResult;
import com.dana.judge.service.ISandboxService;
import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.command.*;
import com.github.dockerjava.api.model.Bind;
import com.github.dockerjava.api.model.HostConfig;
import com.github.dockerjava.api.model.Image;
import com.github.dockerjava.api.model.Volume;
import com.github.dockerjava.core.DefaultDockerClientConfig;
import com.github.dockerjava.core.DockerClientBuilder;
import com.github.dockerjava.netty.NettyDockerCmdExecFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.io.File;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

@Service
public class SandboxServiceImpl implements ISandboxService {

    @Value("${sandbox.docker.host:tcp://localhost:2375}")
    private String dockerHost;

    @Value("${sandbox.limit.memory:100000000}")
    private Long memoryLimit;

    @Value("${sandbox.limit.memory-swap:100000000}")
    private Long memorySwapLimit;

    @Value("${sandbox.limit.cpu:1}")
    private Long cpuLimit;

    @Value("${sandbox.limit.time:5}")
    private Long timeLimit;

    private DockerClient dockerClient;

    private String containerId;

    private String userCodeDir;

    private String userCodeFileName;

    @Override
    public SandBoxExecuteResult exeJavaCode(Long uerId, String userCode, List<String> inputList) {
        //生成一个文件,存放用户提交的代码,使用docker运行,本地文件可以通过挂载数据卷的方式从宿主机传递给docker
        createUserCodeFile(uerId,userCode);
        //创建并运行docker
        initDockerSanBox();
        //编译
        CompileResult compileResult = compileResultByDocker();
        //编译失败
        if (!compileResult.isCompiled()){
            deleteUserCodeFile();//编译失败后删除文件
            deleteContainer();//停止运行,删除容器并且断开docker连接
            return SandBoxExecuteResult.fail(CodeRunStatus.COMPILE_FAILED,compileResult.getExeMessage());
        }
        //执行代码
        return  executeResultJavaCodeByDocker(inputList);
    }





    //创建并且返回用户代码的文件
    private void createUserCodeFile(Long userId,String userCode){
        /**
         * 这行代码的作用是构建一个用于存储考试代码文件的目录路径
         * System.getProperty("user.dir"): 获取当前工作目录的路径
         * File.separator: 系统相关的文件分隔符
         * dana_oj \ user-code类似这样的路径
         */
        String examCodeDir=System.getProperty("user.dir")+ File.separator+ JudgeConstants.EXAM_CODE_DIR;

        //如果该目录下不存在就会创建一个
        if (!FileUtil.exist(examCodeDir)){
            FileUtil.mkdir(examCodeDir);
        }
        //获取时间
        String time= LocalDateTimeUtil.format(LocalDateTime.now(), DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
        //拼接用户代码文件格式:类似user-code\1001_202506030755011
        userCodeDir=examCodeDir+File.separator+userId+ Constants.UNDERLINE_SEPARATOR+time;
        userCodeFileName=userCodeDir+File.separator+JudgeConstants.USER_CODE_JAVA_CLASS_NAME;
        //将用户提交的代码(userCode)写入文件(userCodeFileName)中
        FileUtil.writeString(userCode,userCodeFileName, Constants.UTF8);
    }


    //编译,使用docker编译
    private CompileResult compileResultByDocker(){
        //DOCKER_JAVAC_CMD当前文件通过数据卷挂载到对应的docker目录下,所以要去对应的目录进行编译
        //编译不需要参数(null)
        //containerId容器id,指定去哪个容器执行
        //完整的编译指令cmdId
        String cmdId=createExeCmd(JudgeConstants.DOCKER_JAVAC_CMD,null,containerId);

        //用于获取编译后的结果
        DockerStartResultCallback resultCallback=new DockerStartResultCallback();
        //获取最终结果,编译是否通过以及未通过的错误日志
        CompileResult compileResult=new CompileResult();
        try {
            dockerClient
                    .execStartCmd(cmdId)
                    .exec(resultCallback)
                    .awaitCompletion();
            if (CodeRunStatus.FAILED.equals(resultCallback.getCodeRunStatus())){
                compileResult.setCompiled(false);
                compileResult.setExeMessage(resultCallback.getErrorMessage());
            }else {
                compileResult.setCompiled(true);
            }
            return compileResult;
        }catch (InterruptedException e){
            throw new RuntimeException(e);
        }
    }
    //执行代码
    private SandBoxExecuteResult executeResultJavaCodeByDocker(List<String> inputList) {
        List<String> outputList = new ArrayList<>();//记录输出结果
        long maxMemory = 0L; //最大占用内存
        long maxUseTime = 0L; //最大运行时间
        //执行用户代码
        //因为可能会有多组测试用例,我们都需要执行
        for (String inputArgs : inputList) {
            //cmdId是命令标识,执行命令后会生成一个cmdId,可以定位命令
            String cmdId = createExeCmd(JudgeConstants.DOCKER_JAVA_EXEC_CMD, inputArgs, containerId);
            //开始执行
            StopWatch stopWatch = new StopWatch();             //执行代码后开始计时
            //执行情况监控
            StatsCmd statsCmd = dockerClient.statsCmd(containerId);  //启动监控
            StatisticsCallback statisticsCallback = statsCmd.exec(new StatisticsCallback());
            statsCmd.start();
            DockerStartResultCallback resultCallback = new DockerStartResultCallback();
            try {
                dockerClient.execStartCmd(cmdId)                         //cmdId 命令id,execStartCmd就是执行这条命令
                        .exec(resultCallback)                           //resultCallback作用就是拿到执行结果
                        .awaitCompletion(timeLimit, TimeUnit.SECONDS);  //阻塞等待,超过timeLimit时间限制就不会等待
                if (CodeRunStatus.FAILED.equals(resultCallback.getCodeRunStatus())) {
                    return SandBoxExecuteResult.fail(CodeRunStatus.NOT_ALL_PASSED);
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            stopWatch.stop();     //结束计时
            statsCmd.close();     //结束docker容器执行统计
            long userTime = stopWatch.getLastTaskTimeMillis(); //执行耗时
            maxUseTime = Math.max(userTime, maxUseTime);
            Long memory = statisticsCallback.getMaxMemory();
            if (memory != null) {
                maxMemory = Math.max(maxMemory, statisticsCallback.getMaxMemory());  //记录占用最大内存的测试用例
            }
            outputList.add(resultCallback.getMessage().trim());        //记录正确的输出结果
        }

        deleteContainer();     //删除容器

        deleteUserCodeFile();  //清理文件

        return getSanBoxResult(inputList, outputList, maxMemory, maxUseTime); //封装结果
    }


    private SandBoxExecuteResult getSanBoxResult(List<String> inputList, List<String> outList,
                                                 long maxMemory, long maxUseTime) {
        if (inputList.size() != outList.size()) {
            //输入用例数量 不等于 输出用例数量  属于执行异常
            return SandBoxExecuteResult.fail(CodeRunStatus.NOT_ALL_PASSED, outList, maxMemory, maxUseTime);
        }
        return SandBoxExecuteResult.success(CodeRunStatus.SUCCEED, outList, maxMemory, maxUseTime);
    }

    /**
     * 在指定的Docker容器中创建一个可执行命令，并返回该命令的唯一标识 ID。
     * @param javaCmdArr 要执行的命令数组
     * @param inputArgs 要执行的命令数组
     * @param containerId 指定要在哪个容器中执行命令
     * @return 该命令的唯一 ID。
     */
    private String createExeCmd(String[] javaCmdArr, String inputArgs, String containerId) {
        if (!StrUtil.isEmpty(inputArgs)){
            //当入参不为空的时候拼接入参
            String[] inputArray=inputArgs.split("");//入参
            javaCmdArr= ArrayUtil.append(JudgeConstants.DOCKER_JAVA_EXEC_CMD,inputArray);
        }
        ExecCreateCmdResponse cmdResponse=dockerClient.execCreateCmd(containerId)
                .withCmd(javaCmdArr)
                .withAttachStderr(true)
                .withAttachStdin(true)
                .withAttachStdout(true)
                .exec();
        return cmdResponse.getId();
    }


    private void initDockerSanBox() {
        DefaultDockerClientConfig clientConfig = DefaultDockerClientConfig.createDefaultConfigBuilder()
                .withDockerHost(dockerHost)
                .build();
        //构建docker客户端
        dockerClient = DockerClientBuilder
                .getInstance(clientConfig)
                .withDockerCmdExecFactory(new NettyDockerCmdExecFactory())
                .build();
        //拉取镜像
        pullJavaEnvImage();
        //创建容器  限制资源   控制权限
        HostConfig hostConfig = getHostConfig();
        CreateContainerCmd containerCmd = dockerClient
                .createContainerCmd(JudgeConstants.JAVA_ENV_IMAGE)
                .withName(JudgeConstants.JAVA_CONTAINER_NAME);
        CreateContainerResponse createContainerResponse = containerCmd
                .withHostConfig(hostConfig)
                .withAttachStderr(true)
                .withAttachStdout(true)
                .withTty(true)
                .exec();
        //记录容器id
        containerId = createContainerResponse.getId();
        //启动容器
        dockerClient.startContainerCmd(containerId).exec();
    }

    //拉取java执行环境镜像 需要控制只拉取一次
    private void pullJavaEnvImage() {
        ListImagesCmd listImagesCmd = dockerClient.listImagesCmd();
        List<Image> imageList = listImagesCmd.exec();
        for (Image image : imageList) {
            String[] repoTags = image.getRepoTags();
            if (repoTags != null && repoTags.length > 0 && JudgeConstants.JAVA_ENV_IMAGE.equals(repoTags[0])) {
                return;
            }
        }
        PullImageCmd pullImageCmd = dockerClient.pullImageCmd(JudgeConstants.JAVA_ENV_IMAGE);
        try {
            pullImageCmd.exec(new PullImageResultCallback()).awaitCompletion();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    //限制资源   控制权限
    private HostConfig getHostConfig() {
        HostConfig hostConfig = new HostConfig();
        //设置挂载目录，指定用户代码路径
        hostConfig.setBinds(new Bind(userCodeDir, new Volume(JudgeConstants.DOCKER_USER_CODE_DIR)));
        //限制docker容器使用资源
        hostConfig.withMemory(memoryLimit);
        hostConfig.withMemorySwap(memorySwapLimit);
        hostConfig.withCpuCount(cpuLimit);
        hostConfig.withNetworkMode("none");  //禁用网络
        hostConfig.withReadonlyRootfs(true); //禁止在root目录写文件
        return hostConfig;
    }

    //删除文件
    private void deleteUserCodeFile(){
        FileUtil.del(userCodeDir);
    }

    //执行完毕之后删除容器
    private void deleteContainer(){
        dockerClient.stopContainerCmd(containerId).exec();
        dockerClient.removeContainerCmd(containerId).exec();
        //断开docker连接
        try {
            dockerClient.close();
        }catch (IOException e){
            throw new RuntimeException(e);
        }
    }
}
